﻿#region Apache 2.0 License header

//   Copyright 2012 Lesley van Zijl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License. 

#endregion

using System;
using System.Reflection;
using System.Reflection.Emit;

namespace FavourObjects.ForwardBuilders
{
    internal class ForwardPropertyToPropertyBuilder<TTargetClass>
        : ForwardPropertyBuilderBase<TTargetClass>
        where TTargetClass : class
    {
        private readonly PropertyInfo _targetProperty;

        public ForwardPropertyToPropertyBuilder(TypeBuilder typeBuilder, PropertyBuilder propertyBuilder,
                                                PropertyInfo targetproperty, FieldInfo innerObjectField)
            : base(typeBuilder, propertyBuilder, innerObjectField)
        {
            _targetProperty = targetproperty;
        }

        public MethodInfo CreateGetMethod()
        {
            Type propertyType = _targetProperty.PropertyType;

            MethodBuilder methodBuilder = TypeBuilder.DefineMethod("get_" + _targetProperty.Name,
                                                                   PropertyMethodAttributes,
                                                                   propertyType,
                                                                   Type.EmptyTypes);

            ILGenerator iLGenerator = methodBuilder.GetILGenerator();

            int getTargetPropertyReference = iLGenerator.DeclareLocal(typeof (MethodInfo)).LocalIndex;
            int localTargetTypeField = iLGenerator.DeclareLocal(typeof (TTargetClass)).LocalIndex;

            EmitGetTargetClassType(iLGenerator, localTargetTypeField);

            EmitGetTargetPropertyGetter(iLGenerator, getTargetPropertyReference, localTargetTypeField);

            EmitReturnPropertyValue(iLGenerator, getTargetPropertyReference);

            PropertyBuilder.SetGetMethod(methodBuilder);

            return methodBuilder;
        }

        public MethodInfo CreateSetMethod()
        {
            Type propertyType = _targetProperty.PropertyType;

            MethodBuilder methodBuilder = TypeBuilder.DefineMethod("set_" + _targetProperty.Name,
                                                                   PropertyMethodAttributes,
                                                                   null,
                                                                   new[] {propertyType});

            ILGenerator iLGenerator = methodBuilder.GetILGenerator();

            int setTargetPropertyReference = iLGenerator.DeclareLocal(typeof (MethodInfo)).LocalIndex;


            int localTargetTypeField = iLGenerator.DeclareLocal(typeof (TTargetClass)).LocalIndex;

            EmitGetTargetClassType(iLGenerator, localTargetTypeField);

            EmitGetTargetPropertySetter(iLGenerator, setTargetPropertyReference, localTargetTypeField);

            EmitSetPropertyValue(iLGenerator, setTargetPropertyReference);

            PropertyBuilder.SetSetMethod(methodBuilder);

            return methodBuilder;
        }

        private void EmitSetPropertyValue(ILGenerator iLGenerator, int setTargetPropertyReference)
        {
            Type propertyType = _targetProperty.PropertyType;

            int localObjectArray = iLGenerator.DeclareLocal(typeof (Object[])).LocalIndex;

            iLGenerator.Emit(OpCodes.Ldloc_S, setTargetPropertyReference);
            iLGenerator.Emit(OpCodes.Ldarg_0);
            iLGenerator.Emit(OpCodes.Ldfld, InnerObjectFieldInfo);
            iLGenerator.Emit(OpCodes.Ldc_I4_1);
            iLGenerator.Emit(OpCodes.Newarr, typeof (Object));
            iLGenerator.Emit(OpCodes.Stloc_S, localObjectArray);
            iLGenerator.Emit(OpCodes.Ldloc_S, localObjectArray);
            iLGenerator.Emit(OpCodes.Ldc_I4_0);
            iLGenerator.Emit(OpCodes.Ldarg_1);

            if (propertyType.IsValueType)
            {
                iLGenerator.Emit(OpCodes.Box, propertyType);
            }

            iLGenerator.Emit(OpCodes.Stelem_Ref);
            iLGenerator.Emit(OpCodes.Ldloc_S, localObjectArray);

            MethodInfo invokeMethod = MethodRegistry.GetMethod(typeof (MethodBase), "Invoke", typeof (object),
                                                               typeof (object[]));

            iLGenerator.Emit(OpCodes.Callvirt, invokeMethod);
            iLGenerator.Emit(OpCodes.Pop);
            iLGenerator.Emit(OpCodes.Ret);
        }

        private void EmitGetTargetPropertyMethod(ILGenerator iLGenerator, int setTargetPropertyReference,
                                                 int localTargetTypeField, MethodInfo method)
        {
            iLGenerator.Emit(OpCodes.Ldloc_S, localTargetTypeField);
            iLGenerator.Emit(OpCodes.Ldstr, _targetProperty.Name);
            iLGenerator.Emit(OpCodes.Ldc_I4_S, (byte) BindingFlagsForAllMembers);

            MethodInfo getPropertyMethod = MethodRegistry.GetMethod(typeof (Type), "GetProperty",
                                                                    typeof (String), typeof (BindingFlags));

            iLGenerator.Emit(OpCodes.Callvirt, getPropertyMethod);
            iLGenerator.Emit(OpCodes.Ldc_I4_1);
            iLGenerator.Emit(OpCodes.Callvirt, method);

            iLGenerator.Emit(OpCodes.Stloc_S, setTargetPropertyReference);
        }

        private void EmitGetTargetPropertySetter(ILGenerator iLGenerator, int setTargetPropertyReference,
                                                 int localTargetTypeField)
        {
            MethodInfo getSetMethod = MethodRegistry.GetMethod(typeof (PropertyInfo), "GetSetMethod", typeof (bool));
            EmitGetTargetPropertyMethod(iLGenerator, setTargetPropertyReference, localTargetTypeField, getSetMethod);
        }

        private void EmitGetTargetPropertyGetter(ILGenerator iLGenerator, int setTargetPropertyReference,
                                                 int localTargetTypeField)
        {
            MethodInfo getGetMethod = MethodRegistry.GetMethod(typeof (PropertyInfo), "GetGetMethod", typeof (bool));
            EmitGetTargetPropertyMethod(iLGenerator, setTargetPropertyReference, localTargetTypeField, getGetMethod);
        }

        private void EmitReturnPropertyValue(ILGenerator iLGenerator, int localMethod)
        {
            Type returnType = _targetProperty.PropertyType;

            int localPropertyField = iLGenerator.DeclareLocal(returnType).LocalIndex;

            iLGenerator.Emit(OpCodes.Ldloc_S, localMethod);
            iLGenerator.Emit(OpCodes.Ldarg_0);
            iLGenerator.Emit(OpCodes.Ldfld, InnerObjectFieldInfo);
            iLGenerator.Emit(OpCodes.Ldnull);

            MethodInfo invokeMethod = MethodRegistry.GetMethod(typeof (MethodBase), "Invoke", typeof (object),
                                                               typeof (object[]));

            iLGenerator.Emit(OpCodes.Callvirt, invokeMethod);

            iLGenerator.Emit(returnType.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, returnType);

            iLGenerator.Emit(OpCodes.Stloc_S, localPropertyField);

            iLGenerator.Emit(OpCodes.Ldloc_S, localPropertyField);

            iLGenerator.Emit(OpCodes.Ret);
        }
    }
}